import * as protocol from './protocol_schema.ts';

class Emit {
    deps: string[] = [];
    unstable(status: protocol.VersionStatus) {
        return status.deprecated || status.experimental;
    }
    emitDocComment(description?: string) {
        let ret = '';
        if (description != undefined) {
            ret += '/**';
            ret += this.emitNewLine();
            ret += `* ${description}`;
            ret += this.emitNewLine();
            ret += '*/';
        }
        return ret;
    }

    emitPairs(begin: string, end: string, content = '') {
        return `${begin}${content}${end}`;
    }
    emitBrackets(content = '') {
        return this.emitPairs('[', ']', content);
    }
    emitParentheses(content = '') {
        return this.emitPairs('(', ')', content);
    }
    emitBraces(content = '') {
        return this.emitPairs('{', '}', content);
    }

    emitSemiColon() {
        return ';';
    }
    emitComma() {
        return ',';
    }
    emitNewLine() {
        return '\n';
    }
    emitOptional(optional = false) {
        return `${optional ? '?' : ''}`;
    }
    emitProperty(property: protocol.Property) {
        if (this.unstable(property)) return '';
        return `${property.name} ${this.emitOptional(property.optional)} : ${
            this.emitAnonymousType(property)
        }`;
    }

    emitAnonymousType(ty: protocol.AnonymousType): string {
        let ret = '';

        if ('enum' in ty) {
            return this.emitEnum(ty);
        }

        if ('type' in ty) {
            if (ty.type == 'array' && ('items' in ty)) {
                return `${
                    this.emitAnonymousType((ty as protocol.ArrayType).items)
                } ${this.emitBrackets()}`;
            }
            if (ty.type == 'object') {
                return this.emitObject(ty);
            }
            if (ty.type == 'integer') {
                ret += 'number';
            } else if (ty.type == 'any') {
                ret += 'unknown';
            } else {
                ret += ty.type;
            }
        } else if ('$ref' in ty) {
            const ns = ty.$ref.split('.');
            if (ns.length == 2) {
                if (this.deps.includes(ns[0]) == false) {
                    this.deps.push(ns[0]);
                }
            }
            ret += ty.$ref;
        }
        return ret;
    }

    emitObject(obj: protocol.ObjectType) {
        if ('properties' in obj) {
            return this.emitBraces(
                this.emitPropertyList(obj.properties).join(
                    this.emitSemiColon(),
                ),
            );
        } else {
            return 'unknown';
        }
    }

    emitPropertyList(properties?: protocol.Property[]): string[] {
        const ret = new Array<string>();
        properties?.forEach((property) => {
            const propertyString = this.emitProperty(property);
            if (propertyString != '') {
                ret.push(this.emitProperty(property));
            }
        });
        return ret;
    }

    emitEnum(un: protocol.EnumType) {
        let ret = '';
        if (un.type == 'string') {
            ret += '"';
            ret += un.enum.join('"|"');
            ret += '"';
        } else {
            ret += un.enum.join('|');
        }
        return ret;
    }

    emitTypeAlias(nty: protocol.NamedType) {
        let ret = '';
        if (this.unstable(nty)) return ret;
        ret += this.emitDocComment(nty.description);
        ret += this.emitNewLine();
        if ('type' in nty && nty.type == 'object') {
            ret += `export type ${nty.id} = ${this.emitObject(nty)}`;
        } else {
            ret += `export type ${nty.id} = ${this.emitAnonymousType(nty)}`;
        }
        ret += this.emitSemiColon();
        ret += this.emitNewLine();
        return ret;
    }

    emitImport() {
        let ret = '';
        this.deps?.forEach((dep) => {
            ret += `import * as ${dep} from './${dep}.ts'`;
            ret += this.emitSemiColon();
            ret += this.emitNewLine();
        });
        return ret;
    }
    emitCmdDoc(command: protocol.Command) {
        let ret = '';
        ret += '/**';
        ret += this.emitNewLine();
        ret += `${command.description}`;
        ret += this.emitNewLine();
        command.parameters?.forEach((param) => {
            ret += `@param {${
                this.emitAnonymousType(param)
            }} ${param.name} ${param.description}`;
            ret += this.emitNewLine();
        });
        command.returns?.forEach((r) => {
            ret += `@returns {${
                this.emitAnonymousType(r)
            }} retObj.${r.name} ${r.description}`;
            ret += this.emitNewLine();
        });
        ret += '*/';
        ret = ret.replaceAll('\n', '\n* ');
        return ret;
    }
    emitArrowFuncSig(command: protocol.Command) {
        let ret = '';
        ret += this.emitCmdDoc(command);
        ret += this.emitNewLine();
        ret += `${command.name}:`;
        ret += this.emitParentheses(
            this.emitPropertyList(command.parameters).join(this.emitComma()),
        );
        ret += '=>';
        if (command.returns != undefined) {
            ret += this.emitBraces(`${this.emitPropertyList(command.returns)}`);
        } else {
            ret += 'void';
        }
        ret += this.emitSemiColon();
        return ret;
    }

    // Command 生成接口和参数的对象类型，返回类型（如果有的话）
    emitCommands(domainName: string, commands?: protocol.Command[]): string {
        let ret = '';
        commands?.forEach((cmd) => {
            ret += this.emitCommand(domainName, cmd);
            ret += this.emitNewLine();
        });
        return ret;
    }
    emitCommand(domainName: string, cmd: protocol.Command): string {
        if (this.unstable(cmd)) return '';
        let ret = this.emitCmdDoc(cmd);
        ret += this.emitNewLine();
        ret += `export interface ${cmd.name}Method`;
        let content = '';
        if (cmd.parameters != undefined) {
            content = this.emitCommandBody('params', cmd.parameters);
        }
        if (cmd.returns != undefined) {
            content += this.emitCommandBody('returns', cmd.returns);
        }
        ret += this.emitBraces(
            `method: "${domainName}.${cmd.name}"; ${content}`,
        );
        return ret;
    }
    private emitCommandBody(key: string, properties: protocol.Property[]) {
        let content = '';
        const propertyListString = this.emitPropertyList(properties)
            .join(';');
        if (propertyListString != '') {
            content += `${key}: ${this.emitBraces(propertyListString)}`;
            content += this.emitComma();
        }
        return content;
    }

    emitSignatureList(domain: protocol.Domain) {
        let ret = '';
        domain.commands?.forEach((cmd) => {
            ret += this.emitArrowFuncSig(cmd);
            ret += this.emitNewLine();
        });
        return ret;
    }
    emitEvent(domainName: string, evt: protocol.Event) {
        let ret = '';
        if (this.unstable(evt)) return ret;
        ret += `export interface ${evt.name}Event`;
        let content = '';
        if (evt.parameters != undefined) {
            content += this.emitCommandBody('params', evt.parameters);
        }
        ret += this.emitBraces(
            `method:"${domainName}.${evt.name}"; ${content}`,
        );
        return ret;
    }
    emitEvents(domainName: string, evts?: protocol.Event[]) {
        let ret = '';
        evts?.forEach((evt) =>
            ret += this.emitEvent(domainName, evt) + this.emitNewLine()
        );
        return ret;
    }
    emitDomain(domain: protocol.Domain) {
        let ret = '';
        if (this.unstable(domain)) return '';
        ret += this.emitCommands(domain.domain, domain.commands);
        ret += this.emitEvents(domain.domain, domain.events);
        ret = this.emitTypesAlias(domain, ret);
        ret = this.emitImport() + this.emitNewLine() + ret;

        return (
            `
            /** 
             * DO NOT EDIT THIS FILE DIRECTLY!
             * THIS FILE IS GENEREATED AUTOMATICLY BY DENO SCRIPT
             */
            ` + ret
        );
    }

    emitTypesAlias(domain: protocol.Domain, ret: string) {
        domain.types?.forEach((nty) => {
            ret += this.emitTypeAlias(nty);
        });
        return ret;
    }
}

function writeDomain(domain: protocol.Domain) {
    const emit = new Emit();
    const domainContent = emit.emitDomain(domain);
    if (domainContent != '') {
        return Deno.writeTextFile(
            `./src/vendor/chrome-protocol/${domain.domain}.ts`,
            domainContent,
        );
    }
}

async function process_json(
    json_path: string,
) {
    const content = await Deno.readTextFile(json_path);
    const json_data = JSON.parse(content);
    const writePromises = json_data.domains.map((domain: protocol.Domain) =>
        writeDomain(domain)
    );
    return Promise.all(writePromises);
}

await Deno.remove('src/vendor/chrome-protocol/', { recursive: true }).then(
    () => Deno.mkdir('src/vendor/chrome-protocol'),
).then(() =>
    Promise.all([
        process_json('vendor/chrome-protocol/json/browser_protocol.json'),
        process_json('vendor/chrome-protocol/json/js_protocol.json'),
    ])
).then(() => fmt()).catch((e) => console.log(e));

function fmt(): Deno.ProcessStatus | PromiseLike<Deno.ProcessStatus> {
    return Deno.run({
        cmd: [
            'deno',
            'fmt',
            './src/vendor/chrome-protocol',
            '--config',
            'deno.json',
        ],
    }).status();
}
